Skip to content

Fix enum const-expression case values, extend pointcut expressiveness, add PHP 8.5 limitations doc - #614

Merged
lisachenko merged 5 commits into
masterfrom
claude/php85-audit-fix-enum-pointcuts-docs
Aug 29, 2026
Merged

Fix enum const-expression case values, extend pointcut expressiveness, add PHP 8.5 limitations doc#614
lisachenko merged 5 commits into
masterfrom
claude/php85-audit-fix-enum-pointcuts-docs

Conversation

@lisachenko

Copy link
Copy Markdown
Member

Summary

Four independent changes, one commit each.

1. Enum proxies lose constant-expression case values — Fixes #600

EnumProxyGenerator::resolveEnumData() only recognized String_/Int_ literal case values, so case Negative = -1;, case Shifted = 1 << 2; and case FromConst = self::SHIFT + 10; resolved to null, EnumGenerator::addEnumCase() skipped setValue(), and the proxy declared a pure case inside a backed enum — a PHP fatal error ("Case Negative of backed enum ... must have a value") at proxy load time.

Fix: the parser-reflection path now carries the raw PhpParser Expr node through to EnumGenerator, which emits it verbatim (Builder\EnumCase::setValue() accepts Expr natively). self::CONST keeps resolving on the proxy because class constants stay in the woven trait and trait constants participate in the composing class since PHP 8.2 — verified at runtime, not just lint: a test weaves the fixture enum, loads the woven trait + proxy enum, and asserts Negative->value === -1, Shifted->value === 4, FromConst->value === 12 and from(12) works.

Tests: EnumGeneratorTest (verbatim Expr emission), EnumProxyGeneratorTest (native-reflection path emits evaluated scalars, never valueless cases), WeavingTransformerTest (golden php81-enum-const-expr{,-woven,-proxy}.php fixtures + runtime load check), EnumWeavingTest (end-to-end project fixture ConstExprBackedEnum).

2. Pointcut expressiveness — Fixes #604

(a) ReturnTypePointcut union/intersection/DNF support. Pattern and actual type are normalized into sets of intersection groups (split on | at paren depth 0, then &; parens and leading backslashes removed; leading ? on the actual type expands to |null). Semantics (documented in the class docblock, "not supported yet" note removed):

  • single-type pattern (string, Exc*) matches if any member of the actual type matches (wildcards kept per member);
  • composite pattern (string|int, Countable&Iterator) must correspond one-to-one to the actual member set, order-insensitively;
  • in the pattern, ? keeps its historical single-char-wildcard meaning (BC) — nullable patterns are written Foo|null.

The grammar now accepts union/intersection return types after : (e.g. execution(public Example->method(*): string|int), ...: Countable&Iterator, DNF paren-free as A&B|C which equals (A&B)|C per PHP precedence).

(b) Modifier predicates readonly, private(set), protected(set) for ModifierPointcut/grammar, mapped to ReflectionProperty::IS_READONLY / IS_PRIVATE_SET / IS_PROTECTED_SET. Matching stays bitmask-based: both native reflection and Go\ParserReflection\ReflectionProperty expose these bits via getModifiers() (verified against vendor parser-reflection), so no method_exists guards are needed. The asymmetric tokens are lexed as single tokens (private(set)), keeping full BC. The LALR parse table was regenerated from the updated grammar — zero conflicts, all pre-existing grammar tests pass unchanged.

Deferred (small, deliberate): parenthesized DNF groups in the grammar ((A&B)|C) — the paren-free equivalent parses and matches identically, and ReturnTypePointcut itself normalizes parenthesized patterns when constructed directly; wildcards inside grammar-level return-type members (supported via direct construction). Neither requires further grammar surgery for the issue's use cases.

3. docs/php85-limitations.md — Fixes #605

New document modeled on docs/php84-limitations.md, capturing the PHP 8.5.10 / 8.6.0beta2 audit facts from PR #597: what works (pipe |> in woven bodies, clone with, #[\NoDiscard] propagation with join-point dispatch returning the value, attributes on class constants incl. #[\Deprecated], closures/FCC parameter defaults, final promoted + static asymmetric visibility on non-intercepted properties, self/parent reflection resolution) and what is limited — each phrased as "tracked in #NNN" (#598, #599, #600, #601, #602, #603) since fixes are in flight on other branches — plus the permanent engine constraint that static properties are never interceptable via access() (no property hooks for statics). docs/php84-limitations.md untouched. README links the new page.

4. Hygiene — Partially addresses #610

  • Deleted 12 orphaned fixtures in tests/Instrument/Transformer/_files/ after verifying each basename is unreferenced across tests/ and src/. Note: yii_style.php / yii_style_output.php from the issue's list of 14 are not orphaned — FilterInjectorTransformerTest still reads them — so they were kept.
  • .github/workflows/phpstan.yml: PHP 8.4 + 8.5 matrix with per-version cache keys (php-8.4 / php-8.5).
  • tests/functions.php: typed the Symfony Finder glob() override as glob(string $pattern, int $flags = 0): array — matching how Finder::searchInDirectory() invokes it (string pattern, int GLOB_* bitmask).

Rest of #610 (PARAMETER_WIDENING removal, constructor promotion, replacement coverage fixtures) intentionally not touched — owned by other work.

Test evidence

Gate Result
php8.5 vendor/bin/phpunit OK — 2536 tests, 3042 assertions (1 deprecation, pre-existing on master)
php8.4 vendor/bin/phpunit OK — 2536 tests, 3039 assertions (1 deprecation pre-existing, 1 skipped)
php8.5 vendor/bin/phpstan analyze --memory-limit=1G OK — no errors, level 10, no new baseline entries
php8.6 vendor/bin/phpunit (informational, 8.6.0beta2) OK — 2536 tests, 3042 assertions

Fixes #600
Fixes #604
Fixes #605
Partially addresses #610


Generated by Claude Code

claude added 4 commits August 28, 2026 21:33
EnumProxyGenerator::resolveEnumData() only recognized String_/Int_ literal
case values; any other constant expression (`case Negative = -1;`,
`case Shifted = 1 << 2;`, `case FromConst = self::SHIFT + 10;`) resolved
to null, so EnumGenerator::addEnumCase() skipped setValue() and the proxy
declared a pure case inside a backed enum — a PHP fatal error ("Case
Negative of backed enum ... must have a value") as soon as the proxy
loaded.

The parser-reflection path now passes the raw PhpParser Expr node through
to EnumGenerator, which emits it verbatim in the proxy enum.
`self::CONST` expressions keep resolving on the proxy because class
constants stay in the woven trait and trait constants participate in the
composing class since PHP 8.2 — verified by a runtime functional test
that loads the woven trait plus proxy enum and asserts the case values
(-1, 4, 12) and from() lookups.

Coverage added:
- EnumGeneratorTest: verbatim emission of Expr case values
- EnumProxyGeneratorTest: native-reflection path emits evaluated scalars
- WeavingTransformerTest: golden woven/proxy fixtures for the const-expr
  enum plus a runtime load-and-assert check
- EnumWeavingTest: end-to-end weaving of a project fixture enum with
  constant-expression cases

Fixes #600

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFMYyvE4hYRUoMS8mtKrHP
…dicates

ReturnTypePointcut now supports union, intersection and DNF return
types. Both the pattern and the actual reflection type are normalized
into sets of intersection groups (split on '|' at paren depth 0, then
on '&'; parens and leading backslashes removed; a leading '?' on the
actual type expands to '|null'). A single-type pattern matches if any
member of the actual type matches (wildcards preserved per member); a
composite pattern must correspond one-to-one to the actual member set,
order-insensitively. In the pattern, '?' keeps its historical
single-character-wildcard meaning (BC), so nullable patterns are written
as 'Foo|null'. The "not supported yet" note is replaced by the
documented semantics.

The pointcut grammar accepts union/intersection return-type patterns
after ':' (DNF written paren-free, 'A&B|C' == '(A&B)|C'), and new
member modifier predicates 'readonly', 'private(set)' and
'protected(set)', mapped to ReflectionProperty::IS_READONLY /
IS_PRIVATE_SET / IS_PROTECTED_SET. Matching stays bitmask-based in
ModifierPointcut: both native reflection and parser-reflection expose
these bits via getModifiers(), so no implementation-specific guards are
needed. The LALR parse table was regenerated from the updated grammar
(zero conflicts).

The asymmetric-visibility tokens are lexed as single tokens
('private(set)'), keeping full BC for the existing grammar.

Not implemented (out of scope, noted deliberately):
- Parenthesized DNF groups in the grammar ('(A&B)|C'); the paren-free
  equivalent parses and matches identically, and ReturnTypePointcut
  itself normalizes parenthesized patterns when constructed directly.
- Wildcards inside grammar-level return-type members (direct
  ReturnTypePointcut construction supports them).

Fixes #604

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFMYyvE4hYRUoMS8mtKrHP
Add docs/php85-limitations.md, modeled on docs/php84-limitations.md,
capturing the PHP 8.5.10 / 8.6.0beta2 audit results from PR #597:

Working: pipe operator |> in woven bodies, clone with, #[\NoDiscard]
propagation, attributes on class constants (incl. #[\Deprecated]),
closures/FCC as parameter defaults, final promoted properties and
static asymmetric visibility on non-intercepted properties, and
self/parent reflection resolution.

Limited (tracked in their issues, several with fixes in flight):
closures/FCC in attribute arguments (#601), promoted-property
interception (#599), enum const-expression case values (#600), new in
initializers under INTERCEPT_INITIALIZATIONS (#603), global constants
in attribute args (#602), class-level attributes on woven classes
(#598). Static properties (incl. 8.5 static asymmetric visibility) are
never interceptable via access() — property hooks do not exist for
static properties.

README links the new document next to the PHP 8.4 one.

Fixes #605

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFMYyvE4hYRUoMS8mtKrHP
…lob() override

- Delete 12 fixtures in tests/Instrument/Transformer/_files/ that no
  test or source file references (verified by grepping each basename
  across tests/ and src/): anonymous-class(-transformed),
  file-with-self(-transformed), file-with-self-no-namespace(-transformed),
  php80-file(-transformed), php81-file(-transformed),
  php82-file(-transformed). The yii_style.php / yii_style_output.php
  pair from the original list is NOT deleted — it is still used by
  FilterInjectorTransformerTest.
- Run the PHPStan workflow on a PHP 8.4 + 8.5 matrix with per-version
  cache keys.
- Add parameter and return types to the Symfony\Component\Finder glob()
  override in tests/functions.php; Finder calls it with a string
  pattern and an int flag bitmask, so the typed signature stays
  compatible.

Partially addresses #610

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFMYyvE4hYRUoMS8mtKrHP
Comment thread src/Aop/Pointcut/ReturnTypePointcut.php
The '?' wildcard collides with PHP's nullable-type syntax. NamePointcut
and ReturnTypePointcut no longer treat '?' as a one-character wildcard;
in return-type patterns a leading '?' is now a real nullable marker,
'?Foo' being equivalent to 'Foo|null' on both the pattern and the
actual type.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WFMYyvE4hYRUoMS8mtKrHP
@lisachenko
lisachenko merged commit fc7605c into master Aug 29, 2026
8 checks passed
@lisachenko
lisachenko deleted the claude/php85-audit-fix-enum-pointcuts-docs branch August 29, 2026 07:52
lisachenko pushed a commit that referenced this pull request Aug 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment